Release v0.14.2 — Security, type safety, E2E testing#309
Release v0.14.2 — Security, type safety, E2E testing#309nitrobass24 merged 38 commits intomasterfrom
Conversation
multiprocessing.Queue.put() uses a background feeder thread, so items may not be immediately available for get(block=False) after run_loop() returns. Add _pop_result() helper that retries with a 2s timeout instead of reading once and failing on slow CI runners. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Type-only changes, no behavior modifications. Reduces Pyright error count from 149 to 64. Files fixed: - lftp/lftp.py (33) — pexpect spawn null guards, descriptor pattern type ignores, env dict cast, kill() None handling - ssh/sshcp.py (19) — pexpect before/after narrowing with isinstance guards, replacing != pexpect.EOF checks Pyright couldn't narrow - controller/extract/extract_process.py (14) — datetime module vs class confusion, Optional params, dispatch null guard - web/web_app.py (13) — Bottle DictProperty type ignores on route decorators - controller/auto_queue.py (7) — cls TypeVar fix, None guard on pair_id dict lookup, shadowed variable rename Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y-paths Fix 86 Pyright errors in security-critical paths (Phase 3)
Resolves all remaining type errors in production code. Pyright basic mode now passes with 0 errors, 0 warnings. Files fixed: - common/config.py (18) — type: ignore on intentional TypeVar/cls patterns in dynamic Config property system, cls annotation fix - controller/controller.py (62) — type: ignore on Config Any|None values flowing into typed params, null guards on ModelDiff fields, ScannerResult|None typing on _PairContext attributes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Install runtime deps (requirements.txt) before Pyright so bottle/tblib imports resolve (fixes 12 reportMissingImports errors in CI) - Remove continue-on-error: true — Pyright is now a required check - Add python-typecheck to publish job needs gate Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-modules Fix final 80 Pyright errors — 0 errors in basic mode (Phase 4)
Replace the broken Protractor E2E suite with a modern Playwright-based test suite covering all 5 application pages with 66 tests. Structure: - src/e2e-playwright/ — Playwright project with Page Object pattern - 8 spec files: navigation, dashboard, settings, path-pairs, autoqueue, logs, about, theme - 7 page objects: sidebar, dashboard, settings, path-pairs, autoqueue, logs, about - Shared fixtures with API helpers for config/state management Test approach: - Tests run against a live Docker container (no mocking) - Settings tests verify persistence via API round-trip - Path pairs tests verify CRUD with API cleanup - AutoQueue tests set config flags via API before testing - Dashboard tests gracefully skip when no files present - Delete actions test the two-click confirm safety pattern Removed: - src/e2e/ — old Protractor suite (non-functional since 2022) Added Makefile targets: - make test-e2e — headless Playwright tests - make test-e2e-headed — headed mode for debugging - make test-e2e-report — show HTML report Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/docker/test/e2e/ was part of the old Protractor suite — not referenced by CI, Makefile, or any other code. Contains broken compose files referencing undefined services and ancient Chrome Selenium images. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace all hardcoded delays (waitForTimeout) with condition-based waits using expect.poll() for API state and expect().toHaveAttribute() for DOM state - Fix log page selectors to match actual DOM classes (.record) - Fix dashboard goto() to use domcontentloaded instead of networkidle (SSE keeps connection open indefinitely) - Add save/restore for autoqueue config in beforeEach/afterEach - Add beforeEach cleanup for path-pairs to ensure clean state - Remove unused page destructures - Replace fixed sleep in expandSection with waitFor visible Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace all waitForLoadState('networkidle') with 'domcontentloaded'
across all page objects and specs — SSE stream keeps the connection
open so networkidle never fires
- Add waitForSelector for sidebar nav link after page load to ensure
Angular has rendered before interacting with the DOM
- Fix sidebar page object: restart/theme are <a> elements not <button>
- Fix waitForStream fixture to use sidebar selector instead of
non-existent restart button data-testid
- Update navigateTo to use domcontentloaded
Note: The app's rate limiter (120 req/60s) can be triggered by rapid
test execution. Tests pass individually but may hit 429 when run as
a full suite. Rate limit tuning for test traffic is a follow-up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The app's rate limiter (120 req/60s) triggers 429 errors when running the Playwright E2E test suite, which makes rapid page loads and API calls. Add an env var to disable rate limiting for test environments. Set SEEDSYNC_DISABLE_RATE_LIMIT=1 when starting the test container: docker run -e SEEDSYNC_DISABLE_RATE_LIMIT=1 ... Also add make test-e2e-docker target that starts a throwaway container with rate limiting disabled, runs tests, and tears it down. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updated selectors based on real DOM inspection: - autoqueue: input[type=search], div.button (not <button>), #controls - dashboard: #file-list, .name .text .title for file names - settings: collapsible-header with *ngIf rendering (not CSS hide) - about: three-segment version regex v\d+.\d+.\d+ Spec fixes: - Removed dead autoqueue cleanup loop - Added row-gone assertion after path-pair delete - Settings: restore log format, check POST/DELETE responses - Advanced LFTP collapse test checks app-option count Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive DOM-verified selector fixes across all page objects and spec files: Page objects: - dashboard: Bootstrap dropdown selectors, #file-list, .actions button - settings: XPath-based section finding, card body visibility check - path-pairs: .path-pairs, .pair-row, .btn-add, .btn-edit, .btn-delete - autoqueue: #add-pattern .button, input[type=search], force click - logs: #log-search, #log-level, .log-filters, #logs .record Specs: - dashboard: dropdown click-to-open pattern for filter/sort tests - settings: correct section names, config path fixes, CSRF headers - path-pairs: wait for SSE delivery, CSRF Origin headers on mutations - autoqueue: afterEach cleanup, enabled state wait, pattern API format - logs: log filters visibility test - Added apiFetch fixture with Origin header for CSRF-protected endpoints 11 tests skip by design (no files on remote, no log records). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Run E2E tests in the build-test job after the Docker image builds: 1. Start container with SEEDSYNC_DISABLE_RATE_LIMIT=1 2. Install Playwright + Chromium 3. Run 66 tests against the live container 4. Upload HTML report artifact on failure 5. Always stop the container This runs on PRs (same trigger as build-test). Release builds use the separate build-amd64/build-arm64 jobs which don't run E2E. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
package-lock.json is gitignored at the repo root level, so npm ci fails with "missing lockfile". Use npm install instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused page destructures from settings spec tests - Wrap path-pair-dependent test in try/finally so cleanup always runs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Playwright E2E test suite — 66 tests across all pages
Add validation to reject ASCII control characters (0x00-0x1F, 0x7F) in filenames to prevent shell command injection and log injection attacks via CR, LF, tab, and other control characters. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /server/config/get now returns "********" for remote_password and api_key instead of plaintext values - /server/config/set no longer echoes sensitive values in responses - Frontend OptionComponent skips sending updates when password field value equals the redacted sentinel (prevents overwriting stored password with "********") - Added Config.REDACTED_SENTINEL, Config.is_sensitive(), and Config.sensitive_property_names() for centralized sensitivity checks - 5 new tests (3 unit, 2 integration) Closes #257 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verify that invalid filenames are rejected before reaching the controller by adding queue_command.assert_not_called() to all control character rejection tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix 5 pre-existing test failures: change test data from absolute path "/value/with/slashes" to relative "value/with/slashes" — the path validation correctly rejects absolute paths, the tests were outdated - Add queue_command.assert_not_called() to all control char rejection tests to verify invalid filenames never reach the controller All 12 tests now pass (5 pre-existing + 7 new). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reject setting sensitive fields to "********" (the redacted sentinel) with 400 error — prevents accidentally overwriting real credentials via direct API access - Change sensitive_property_names from classmethod to staticmethod (cls was unused) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps the angular group in /src/angular with 9 updates: | Package | From | To | | --- | --- | --- | | [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.4` | `21.2.5` | | [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.4` | `21.2.5` | | [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.4` | `21.2.5` | | [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.4` | `21.2.5` | | [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.4` | `21.2.5` | | [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.4` | `21.2.5` | | [@angular/build](https://github.com/angular/angular-cli) | `21.2.2` | `21.2.3` | | [@angular/cli](https://github.com/angular/angular-cli) | `21.2.2` | `21.2.3` | | [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.4` | `21.2.5` | Updates `@angular/common` from 21.2.4 to 21.2.5 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/v21.2.5/packages/common) Updates `@angular/compiler` from 21.2.4 to 21.2.5 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/v21.2.5/packages/compiler) Updates `@angular/core` from 21.2.4 to 21.2.5 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/v21.2.5/packages/core) Updates `@angular/forms` from 21.2.4 to 21.2.5 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/v21.2.5/packages/forms) Updates `@angular/platform-browser` from 21.2.4 to 21.2.5 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/v21.2.5/packages/platform-browser) Updates `@angular/router` from 21.2.4 to 21.2.5 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/v21.2.5/packages/router) Updates `@angular/build` from 21.2.2 to 21.2.3 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](angular/angular-cli@v21.2.2...v21.2.3) Updates `@angular/cli` from 21.2.2 to 21.2.3 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](angular/angular-cli@v21.2.2...v21.2.3) Updates `@angular/compiler-cli` from 21.2.4 to 21.2.5 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/v21.2.5/packages/compiler-cli) --- updated-dependencies: - dependency-name: "@angular/common" dependency-version: 21.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/compiler" dependency-version: 21.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/core" dependency-version: 21.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/forms" dependency-version: 21.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/platform-browser" dependency-version: 21.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/router" dependency-version: 21.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/build" dependency-version: 21.2.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/cli" dependency-version: 21.2.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/compiler-cli" dependency-version: 21.2.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular ... Signed-off-by: dependabot[bot] <support@github.com>
…angular/develop/angular-d0bae5d802 chore(deps): bump the angular group in /src/angular with 9 updates
…velop/actions/setup-python-6 chore(deps): bump actions/setup-python from 5 to 6
Bumps [jsdom](https://github.com/jsdom/jsdom) from 29.0.0 to 29.0.1. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](jsdom/jsdom@v29.0.0...v29.0.1) --- updated-dependencies: - dependency-name: jsdom dependency-version: 29.0.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
…angular/develop/jsdom-29.0.1 chore(deps-dev): bump jsdom from 29.0.0 to 29.0.1 in /src/angular
Redact sensitive credentials from API responses
…ename Reject control characters in decoded filenames
The Python backend sends null for local_size/remote_size when a file doesn't exist locally/remotely, but the TypeScript interface declared them as `number`. Fix to `number | null` to match the actual JSON contract. Add tests covering isValidatable and validateTooltip when sizes are null. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughMigrates E2E from Protractor to Playwright, removes legacy Protractor Docker E2E infra, adds ~55 Playwright tests and page objects/fixtures, updates CI to run Playwright and enforce Pyright, introduces config redaction sentinel with server-side handling, rejects control characters in filenames, and applies multiple Python typing and controller locking fixes. Changes
Sequence Diagram(s)sequenceDiagram
participant GH as GitHub Actions
participant Docker as Test Container
participant Playwright as Playwright Runner
participant WebApp as SeedSync WebApp
participant Config as Config Serializer
GH->>Docker: start test-container (SEEDSYNC_DISABLE_RATE_LIMIT=1)
GH->>Playwright: run "npx playwright test" (BASE_URL)
Playwright->>WebApp: HTTP requests to UI & API
WebApp->>Config: SerializeConfig.config() -> redact sensitive fields
Config-->>WebApp: return redacted JSON (REDACTED_SENTINEL)
Playwright->>WebApp: POST /server/config/set (sensitive = REDACTED_SENTINEL)
WebApp-->>Playwright: 400 Bad Request (reject sentinel)
Playwright-->>GH: report test failure
GH->>GH: upload playwright-report artifact on failure
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/python/controller/controller.py (1)
285-297:⚠️ Potential issue | 🔴 CriticalDo not revive the legacy default pair when all configured pairs are disabled.
Line 288 marks the controller as idle when every configured pair is disabled, but Lines 291-297 still create a default pair from
config.lftp. If those legacy paths are populated, the app keeps scanning/syncing even though the user explicitly disabled every pair.Minimal fix
if not enabled_pairs: if pairs: # All configured pairs are disabled — signal idle state self.__context.status.controller.no_enabled_pairs = True self.logger.warning("All path pairs are disabled. Enable a pair in Settings to start syncing.") + return [] # Backward compatibility: no path pairs configured, use config.lftp return [🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/python/controller/controller.py` around lines 285 - 297, The code currently returns a legacy default pair from self._create_pair_context using self.__context.config.lftp even when there are configured pairs that are all disabled; change the branching so that when enabled_pairs is empty and pairs is non-empty you set self.__context.status.controller.no_enabled_pairs = True and return an empty list (no default pair), and only fall back to creating the legacy default pair when there are zero configured pairs (i.e., pairs is empty). Update the block that references enabled_pairs, pairs, self._create_pair_context, and self.__context.config.lftp accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 175-210: In the "Install Playwright" step replace the
non-deterministic install command: change the npm invocation in the step named
"Install Playwright" (currently running "npm install" in working-directory
src/e2e-playwright) to "npm ci" so dependencies are installed exactly from
package-lock.json for reproducible E2E runs; keep the subsequent "npx playwright
install --with-deps chromium" line and all other step behavior the same.
In `@CHANGELOG.md`:
- Around line 16-21: Add a new top-level changelog entry for v0.14.2 (semver and
YYYY-MM-DD) and move the "Removed dead Docker E2E infrastructure" bullet out of
the "Changed" section into a new "Removed" section, and if any security fixes
are present, add a "Security" section and move the relevant security bullet(s)
there; ensure the entry includes the standard sections: Changed, Added, Fixed,
Removed, Security (as applicable) and that the existing Angular and CI bullets
remain under "Changed".
In `@Makefile`:
- Around line 44-55: The test target test-e2e-docker currently swallows the
Playwright exit code via "|| true"; change the flow so you capture the exit code
from the npx playwright test command (store its exit status), always run the
cleanup step (docker rm -f seedsync-e2e-test), and then exit/make return using
that captured status; update the sequence around the "cd src/e2e-playwright &&
BASE_URL=... npx playwright test" invocation to save its exit code and use that
value for the final exit while keeping the container cleanup unconditional.
In `@src/angular/src/app/pages/settings/option.component.ts`:
- Around line 57-62: The check in OptionComponent that compares value ===
'********' is fragile because the sentinel is hardcoded; replace this literal
with a shared constant (e.g., REDACTED_SENTINEL) imported from a central
constants module and use that in the conditional that guards password updates
(the block referencing OptionType.Password and the value comparison in
option.component.ts); add the sentinel to a shared constants file (or bridge
from backend config) so both frontend and backend reference the same
REDACTED_SENTINEL constant and update the import/usage in the function that
calls this.type().
In `@src/e2e-playwright/.gitignore`:
- Around line 1-4: The .gitignore inside src/e2e-playwright only affects that
subtree, so add a repo-root ignore entry for /test-results/ (and optionally
/playwright-report/ and node_modules if needed) in the repository root
.gitignore and remove the generated test-results/.last-run.json from the commit
(unstage/remove it from tracking) so it is no longer tracked; locate references
to the existing src/e2e-playwright/.gitignore patterns and mirror the needed
pattern (/test-results/) at the repo root and then git rm --cached the
test-results/.last-run.json before recommitting.
In `@src/e2e-playwright/tests/dashboard.spec.ts`:
- Around line 113-124: The current test "Queue button appears for queueable
files" is vacuous because expect(typeof queueVisible).toBe("boolean") always
passes; update the test to assert meaningful behavior by either removing it or
replacing the assertion: after clicking a row via
dashboard.getFileRows().first() verify the action area is rendered (use a
selector or helper such as dashboard.getActionArea(row).isVisible()) or iterate
dashboard.getFileRows() and call dashboard.getActionButton(row, "queue") for
each row, compute queueVisible = await queueBtn.isVisible().catch(() => false)
and assert that at least one queueVisible is true; update references to queueBtn
and queueVisible accordingly so the test fails when no action area or queue
button is present.
In `@src/e2e-playwright/tests/fixtures.ts`:
- Around line 34-40: The apiGet fixture currently calls fetch without an Origin
header; update the apiGet implementation (the apiGet async fixture function) to
include the same Origin header behavior as apiFetch by passing a headers object
with "Origin": appUrl (or the appropriate origin) when calling fetch so GET
requests include an Origin for consistency with apiFetch.
In `@src/e2e-playwright/tests/logs.spec.ts`:
- Around line 63-70: The derived searchTerm can be empty, causing the subsequent
filter check (records, logs.searchInput.fill,
expect.poll(...).toBeLessThanOrEqual(count)) to be vacuous; guard against this
by detecting an empty searchTerm after deriving it from
records.first().textContent() and either (a) select a sane fallback token from
firstText (e.g., the first non-empty token) or (b) fail/skip the test with a
clear message so the filtering assertion isn't run with an empty query; update
the logic around the searchTerm variable and the call to logs.searchInput.fill
to use the non-empty fallback or short-circuit the assertion when searchTerm ===
"".
In `@src/e2e-playwright/tests/pages/path-pairs.page.ts`:
- Around line 40-52: The current form field selection in path-pairs.page.ts uses
fragile positional selectors (form.locator("input").first(), .nth(1), .nth(2)) —
update the selectors for nameInput, remoteInput, and localInput to use stable
identifiers (e.g., input[name="..."], data-testid attributes, or associated
label text via locator('label:has-text("...")').locator('.. input')) so each
branch that references fields.name, fields.remotePath, and fields.localPath
targets the correct input even if DOM order changes; change the locator
expressions for the variables nameInput, remoteInput, and localInput
accordingly.
- Around line 33-53: The fillForm method's parameter type declares unused fields
enabled and autoQueue; remove those properties from the type so the signature
becomes async fillForm(fields: { name?: string; remotePath?: string; localPath?:
string; }) in the path-pairs.page.ts file (method fillForm in that page class),
and run a quick grep for any callsites of fillForm to ensure none rely on
enabled or autoQueue—if any do, remove those args or handle them where
appropriate.
In `@src/e2e-playwright/tests/pages/sidebar.page.ts`:
- Around line 29-33: The navigateTo method uses waitForLoadState and a sidebar
selector which is unreliable for SPA navigation; replace the DOM load/state wait
and the 'a[href="/dashboard"]' selector check with a route-aware wait: after
await link.click() call await this.page.waitForURL(...) targeting the expected
route (or a pattern) so navigateTo (in sidebar.page.ts) waits for the actual URL
change rather than DOMContentLoaded or an existing sidebar link.
In `@src/e2e-playwright/tests/path-pairs.spec.ts`:
- Around line 54-72: The poll callback used inside expect.poll that calls
apiFetch("/server/pathpairs") should validate the HTTP response (res.ok) before
calling res.json(); update the callback in the block using expect.poll (and the
similar callbacks at the other poll sites) to check res.ok and either throw or
return undefined when the response is not OK so the poll keeps retrying instead
of crashing; locate the callback that awaits apiFetch and uses pairs.find
(referencing apiFetch and expect.poll) and add the res.ok check and appropriate
early return/throw to make the polling robust.
In `@src/e2e-playwright/tests/settings.spec.ts`:
- Around line 90-123: The test "select dropdown changes value and saves to
backend" declares an unused destructured parameter page in its async test
function; remove page from the parameter list (change async ({ page, apiGet })
to async ({ apiGet })) in the test declaration so only apiGet is destructured,
and verify there are no other references to page in this test (function name:
the test with description "select dropdown changes value and saves to backend",
helper: settings.getSelect).
In `@src/python/common/config.py`:
- Around line 468-478: Update the return type annotation of the static method
sensitive_property_names to use the project's typing conventions (e.g.,
typing.Dict and typing.Set) instead of built-in generics; ensure the module
imports the appropriate symbols (or uses fully-qualified typing.Dict/typing.Set)
and change the signature from def sensitive_property_names() -> dict[str,
set[str]] to use Dict[str, Set[str]] so the method and its return type follow
the codebase convention.
In `@src/python/controller/controller.py`:
- Around line 685-695: The assertions inside the critical section can raise and
skip releasing the lock (self.__model_lock), so wrap the model mutation block
that calls self.__model.add_file / remove_file / update_file and computes
diff_file in a safe construct: either replace the manual acquire/release with a
context manager (with self.__model_lock:) or use try/finally to ensure
self.__model_lock.release() always runs before any assertion or exception can
propagate (also apply the same fix around the nearby block at lines handling
assertions 727-729); keep the existing assertions but move them outside the
non-exception-safe lock or protect them inside the try/finally/with to guarantee
the lock is always released.
In `@src/python/lftp/lftp.py`:
- Line 496: The function signature for queue(...) uses a non-parameterized type
for exclude_patterns which loses element type information; update the parameter
to use a list of strings (exclude_patterns: list[str] | None) in the queue
function definition so callers and type checkers know the elements are str
(refer to the queue function and its uses around line 516 to confirm usage).
In `@src/python/tests/integration/test_web/test_handler/test_config.py`:
- Around line 95-100: Add a negative-path integration test to assert that
attempting to SET a sensitive field to the redacted sentinel is rejected: create
a new test (e.g. test_set_sensitive_field_rejects_redacted_sentinel) similar to
test_set_sensitive_field_does_not_echo_value that calls the same endpoint
("/server/config/set/lftp/remote_password/<REDACTED_SENTINEL>"), asserts a
non-200 or explicit rejection response and an error message indicating the
redacted sentinel is not allowed, and finally asserts that
self.context.config.lftp.remote_password remains unchanged; reference the
existing test_set_sensitive_field_does_not_echo_value, the GET/SET endpoint
string, and self.context.config.lftp.remote_password to locate where to add this
test.
In `@src/python/tests/integration/test_web/test_handler/test_controller.py`:
- Around line 192-240: Add the missing assertion to test_path_traversal_rejected
in TestControllerHandlerValidation: after asserting resp.status_int == 400, also
assert that self.controller.queue_command was not called (use
self.controller.queue_command.assert_not_called()) so the test mirrors the other
negative filename tests; locate the assertion to add in the
test_path_traversal_rejected method referencing self.controller.queue_command.
In `@src/python/tests/unittests/test_web/test_serialize/test_serialize_config.py`:
- Around line 91-114: Update the two assertions that expect the redaction
sentinel to use the shared constant instead of the hardcoded string: replace the
literal "********" in test_redacts_remote_password and test_redacts_api_key with
Config.REDACTED_SENTINEL so both tests assert
out_dict["lftp"]["remote_password"] == Config.REDACTED_SENTINEL and
out_dict["web"]["api_key"] == Config.REDACTED_SENTINEL; keep other assertions
unchanged and ensure SerializeConfig.config and Config.REDACTED_SENTINEL are
imported/available in the test module.
---
Outside diff comments:
In `@src/python/controller/controller.py`:
- Around line 285-297: The code currently returns a legacy default pair from
self._create_pair_context using self.__context.config.lftp even when there are
configured pairs that are all disabled; change the branching so that when
enabled_pairs is empty and pairs is non-empty you set
self.__context.status.controller.no_enabled_pairs = True and return an empty
list (no default pair), and only fall back to creating the legacy default pair
when there are zero configured pairs (i.e., pairs is empty). Update the block
that references enabled_pairs, pairs, self._create_pair_context, and
self.__context.config.lftp accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 52ed1ce8-4492-46eb-a11e-c497168bb125
⛔ Files ignored due to path filters (11)
src/angular/package-lock.jsonis excluded by!**/package-lock.json,!**/package-lock.jsonsrc/docker/test/e2e/remote/files/clients.jpgis excluded by!**/*.jpgsrc/docker/test/e2e/remote/files/crispycat/cat.mp4is excluded by!**/*.mp4src/docker/test/e2e/remote/files/documentation.pngis excluded by!**/*.pngsrc/docker/test/e2e/remote/files/goose/goose.mp4is excluded by!**/*.mp4src/docker/test/e2e/remote/files/illusion.jpgis excluded by!**/*.jpgsrc/docker/test/e2e/remote/files/joke/joke.pngis excluded by!**/*.pngsrc/docker/test/e2e/remote/files/testing.gifis excluded by!**/*.gifsrc/docker/test/e2e/remote/files/áßç déÀ.mp4is excluded by!**/*.mp4src/docker/test/e2e/remote/files/üæÒ/µ®© ÷úƤ.pngis excluded by!**/*.pngsrc/docker/test/e2e/remote/id_rsa.pubis excluded by!**/*.pub
📒 Files selected for processing (68)
.github/workflows/ci.ymlCHANGELOG.mdMakefilesrc/angular/package.jsonsrc/angular/src/app/pages/settings/option.component.tssrc/angular/src/app/services/files/view-file.service.spec.tssrc/docker/test/e2e/Dockerfilesrc/docker/test/e2e/chrome/Dockerfilesrc/docker/test/e2e/compose-dev.ymlsrc/docker/test/e2e/compose.ymlsrc/docker/test/e2e/configure/Dockerfilesrc/docker/test/e2e/configure/setup_seedsync.shsrc/docker/test/e2e/parse_seedsync_status.pysrc/docker/test/e2e/remote/Dockerfilesrc/docker/test/e2e/run_tests.shsrc/docker/test/e2e/urls.tssrc/e2e-playwright/.gitignoresrc/e2e-playwright/package.jsonsrc/e2e-playwright/playwright.config.tssrc/e2e-playwright/tests/about.spec.tssrc/e2e-playwright/tests/autoqueue.spec.tssrc/e2e-playwright/tests/dashboard.spec.tssrc/e2e-playwright/tests/fixtures.tssrc/e2e-playwright/tests/logs.spec.tssrc/e2e-playwright/tests/navigation.spec.tssrc/e2e-playwright/tests/pages/about.page.tssrc/e2e-playwright/tests/pages/autoqueue.page.tssrc/e2e-playwright/tests/pages/dashboard.page.tssrc/e2e-playwright/tests/pages/logs.page.tssrc/e2e-playwright/tests/pages/path-pairs.page.tssrc/e2e-playwright/tests/pages/settings.page.tssrc/e2e-playwright/tests/pages/sidebar.page.tssrc/e2e-playwright/tests/path-pairs.spec.tssrc/e2e-playwright/tests/settings.spec.tssrc/e2e-playwright/tests/theme.spec.tssrc/e2e/.gitignoresrc/e2e/README.mdsrc/e2e/conf.tssrc/e2e/package.jsonsrc/e2e/tests/about.page.spec.tssrc/e2e/tests/about.page.tssrc/e2e/tests/app.spec.tssrc/e2e/tests/app.tssrc/e2e/tests/autoqueue.page.spec.tssrc/e2e/tests/autoqueue.page.tssrc/e2e/tests/dashboard.page.spec.tssrc/e2e/tests/dashboard.page.tssrc/e2e/tests/settings.page.spec.tssrc/e2e/tests/settings.page.tssrc/e2e/tsconfig.jsonsrc/e2e/urls.tssrc/python/common/config.pysrc/python/controller/auto_queue.pysrc/python/controller/controller.pysrc/python/controller/extract/extract_process.pysrc/python/lftp/lftp.pysrc/python/ssh/sshcp.pysrc/python/tests/integration/test_web/test_handler/test_config.pysrc/python/tests/integration/test_web/test_handler/test_controller.pysrc/python/tests/unittests/test_controller/test_scan/test_scanner_process.pysrc/python/tests/unittests/test_web/test_serialize/test_serialize_config.pysrc/python/web/handler/config.pysrc/python/web/handler/controller.pysrc/python/web/security.pysrc/python/web/serialize/serialize_config.pysrc/python/web/web_app.pysrc/python/web/web_app_builder.pytest-results/.last-run.json
💤 Files with no reviewable changes (26)
- src/e2e/.gitignore
- src/docker/test/e2e/configure/Dockerfile
- src/docker/test/e2e/chrome/Dockerfile
- src/e2e/tests/settings.page.spec.ts
- src/e2e/tests/about.page.spec.ts
- src/docker/test/e2e/compose-dev.yml
- src/e2e/tsconfig.json
- src/e2e/tests/app.spec.ts
- src/docker/test/e2e/parse_seedsync_status.py
- src/docker/test/e2e/Dockerfile
- src/e2e/tests/about.page.ts
- src/docker/test/e2e/configure/setup_seedsync.sh
- src/e2e/README.md
- src/docker/test/e2e/remote/Dockerfile
- src/e2e/urls.ts
- src/docker/test/e2e/compose.yml
- src/e2e/tests/app.ts
- src/docker/test/e2e/urls.ts
- src/e2e/tests/autoqueue.page.spec.ts
- src/e2e/conf.ts
- src/e2e/tests/dashboard.page.spec.ts
- src/e2e/tests/autoqueue.page.ts
- src/e2e/package.json
- src/e2e/tests/settings.page.ts
- src/e2e/tests/dashboard.page.ts
- src/docker/test/e2e/run_tests.sh
src/python/tests/unittests/test_web/test_serialize/test_serialize_config.py
Show resolved
Hide resolved
- CI: use npm ci for deterministic Playwright installs - CHANGELOG: split Security/Removed sections per semver convention - Makefile: propagate Playwright exit code instead of swallowing it - Frontend: extract REDACTED_SENTINEL constant from hardcoded string - Root .gitignore: add /test-results/, untrack committed file - E2E: fix vacuous dashboard test, add Origin to apiGet, guard empty search term, use label-based selectors in path-pairs form, use waitForURL in sidebar navigation, validate poll responses, remove unused destructured params - Python: try/finally for model_lock safety, parameterize lftp.queue exclude_patterns type, return empty list when all pairs disabled - Tests: add assert_not_called to path traversal test, add redacted sentinel rejection integration test, use Config.REDACTED_SENTINEL constant in serialize tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/python/controller/controller.py (1)
1-1:⚠️ Potential issue | 🟡 MinorFix ruff formatting to pass CI.
The pipeline reports
ruff format --check failedfor this file. Runruff format src/python/controller/controller.pyto fix formatting.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/python/controller/controller.py` at line 1, Run the ruff formatter on the file to fix formatting errors reported by CI: execute `ruff format src/python/controller/controller.py` (or apply the same formatting changes via your editor/IDE) so the file complies with ruff style rules; then stage the modified file and re-run the tests to ensure `ruff format --check` passes.
♻️ Duplicate comments (1)
src/python/tests/integration/test_web/test_handler/test_controller.py (1)
237-241:⚠️ Potential issue | 🟡 MinorAdd missing assertion for
queue_command.assert_not_called().The
test_path_traversal_rejectedtest verifies the 400 status but doesn't assert thatqueue_commandwas not called, unlike all other negative tests in this class (lines 199, 205, 211, 217, 223).🐛 Proposed fix to add missing assertion
def test_path_traversal_rejected(self): uri = quote(quote("../etc/passwd", safe=""), safe="") resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True) self.assertEqual(400, resp.status_int) + self.controller.queue_command.assert_not_called()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/python/tests/integration/test_web/test_handler/test_controller.py` around lines 237 - 241, The test_path_traversal_rejected test checks for a 400 response but misses asserting that controller.queue_command was not invoked; update the test by adding a call to self.controller.queue_command.assert_not_called() at the end of test_path_traversal_rejected to mirror other negative tests (see tests at lines with similar patterns like the ones asserting queue_command.assert_not_called()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Makefile`:
- Around line 93-95: The help output in the Makefile is missing the
test-e2e-docker target; update the help echo block alongside the existing
test-e2e, test-e2e-headed, and test-e2e-report lines to include a brief
description for test-e2e-docker (e.g., "test-e2e-docker - Run E2E tests inside a
fresh Docker container") so users can discover this target; modify the same echo
group in the Makefile where the other test-e2e help lines are defined to add the
new entry.
In `@src/e2e-playwright/tests/dashboard.spec.ts`:
- Around line 108-110: The test's action-button selector is wrong: update the
locator used where actionButtons is assigned (the row.locator(".actions button")
call in dashboard.spec.ts) to match the actual DOM (use ".actions .button") or a
combined selector like ".actions .button, .actions button" so the actionButtons
count check (btnCount >= 1) correctly finds the div.button action controls.
In `@src/e2e-playwright/tests/pages/path-pairs.page.ts`:
- Around line 16-20: The goto() method in PathPairsPage uses
waitForLoadState("domcontentloaded") which can be flaky for Angular/SPAs;
replace or augment that with await this.page.waitForURL("/settings") (mirroring
SidebarPage.navigateTo()) to wait for SPA navigation completion, and keep the
existing waitForSelector('a[href="/dashboard"]', { timeout: 10_000 }) as a
fallback to ensure the page is ready; update the goto() implementation to use
waitForURL("/settings") instead of waitForLoadState or in addition to it.
In `@src/e2e-playwright/tests/path-pairs.spec.ts`:
- Around line 148-151: The test is using positional selectors on the 'inputs'
locator while the page object method 'fillForm' uses label-based selectors;
update the expectations to use form.getByLabel(...) (or locator.getByLabel) with
the same label texts used in 'fillForm' instead of inputs.nth(1)/nth(2) so the
test checks the values by label (e.g., assert the "Name", "Remote", "Local"
labeled inputs have the expected values) to keep selector strategy consistent
and maintainable.
In `@src/e2e-playwright/tests/settings.spec.ts`:
- Around line 35-51: The test "text field change saves to backend" mutates
persisted config (Server Address) without restoring it; capture the original
value by calling apiGet("/server/config/get") and reading
config.lftp.remote_address before changing the field, then ensure you restore
that original value in a finally block (or test.afterEach) — e.g., after the
poll completes (or on error) call the API to set the config back (use your
existing API helper similar to apiGet, e.g., apiPost or apiPut to the
appropriate config endpoint) so the UI test (settings.getTextInput("Server
Address")) leaves the backend unchanged; apply the same pattern to the other
test that changes Server Address (lines ~186-193).
- Around line 165-166: The test uses a fixed path-pair name ("temp-pair") which
causes collisions; change the literal name assignment (the object with name:
"temp-pair" used when creating the path pair in the settings test) to a
generated unique string (e.g., append a timestamp/UUID or short random suffix)
so each test run uses a distinct name; update any subsequent assertions or
lookups that reference that name to use the same generated variable rather than
the hard-coded "temp-pair".
In `@src/python/controller/controller.py`:
- Around line 296-297: Add a startup validation step that asserts required LFTP
config fields are non-None instead of using scattered "# type: ignore[arg-type]"
comments: implement a method like Controller._validate_lftp_config (or a general
Controller.validate_config) called early in Controller.__init__ or
Controller.start that checks self.__context.config.lftp.remote_address,
remote_path, local_path (and any other Optional[str] fields used later) and
raises a clear ValueError (or logs and exits) if any are missing; once this
validation exists, remove the corresponding type ignores (e.g., on uses of
self.__context.config.lftp.remote_path and local_path) so the runtime contract
is explicit and static typing is satisfied.
- Line 1261: The fallback path expression assigning dest_path in
__spawn_move_process is unreachable and unsafe (dest_path = pc.local_path if pc
else self.__pair_contexts[0].local_path); remove the fallback and require pc to
be non-None, or add an explicit validation before using self.__pair_contexts[0]
(e.g., raise a clear exception if pc is None or if __pair_contexts is empty).
Update __spawn_move_process to only use pc.local_path (or validate and raise)
and adjust callers if needed so the function contract guarantees pc is provided.
---
Outside diff comments:
In `@src/python/controller/controller.py`:
- Line 1: Run the ruff formatter on the file to fix formatting errors reported
by CI: execute `ruff format src/python/controller/controller.py` (or apply the
same formatting changes via your editor/IDE) so the file complies with ruff
style rules; then stage the modified file and re-run the tests to ensure `ruff
format --check` passes.
---
Duplicate comments:
In `@src/python/tests/integration/test_web/test_handler/test_controller.py`:
- Around line 237-241: The test_path_traversal_rejected test checks for a 400
response but misses asserting that controller.queue_command was not invoked;
update the test by adding a call to
self.controller.queue_command.assert_not_called() at the end of
test_path_traversal_rejected to mirror other negative tests (see tests at lines
with similar patterns like the ones asserting
queue_command.assert_not_called()).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 080c0ca5-f4f0-4958-ba0b-21cb381668b5
📒 Files selected for processing (18)
.github/workflows/ci.yml.gitignoreCHANGELOG.mdMakefilesrc/angular/src/app/models/config.tssrc/angular/src/app/pages/settings/option.component.tssrc/e2e-playwright/tests/dashboard.spec.tssrc/e2e-playwright/tests/fixtures.tssrc/e2e-playwright/tests/logs.spec.tssrc/e2e-playwright/tests/pages/path-pairs.page.tssrc/e2e-playwright/tests/pages/sidebar.page.tssrc/e2e-playwright/tests/path-pairs.spec.tssrc/e2e-playwright/tests/settings.spec.tssrc/python/controller/controller.pysrc/python/lftp/lftp.pysrc/python/tests/integration/test_web/test_handler/test_config.pysrc/python/tests/integration/test_web/test_handler/test_controller.pysrc/python/tests/unittests/test_web/test_serialize/test_serialize_config.py
| const actionButtons = row.locator(".actions button"); | ||
| const btnCount = await actionButtons.count(); | ||
| expect(btnCount).toBeGreaterThanOrEqual(1); |
There was a problem hiding this comment.
Fix action-button selector to match actual DOM pattern.
Line 108 targets .actions button, but this codebase’s file actions are intentionally rendered as <div class="button">. This can make the assertion fail even when actions are present.
♻️ Suggested fix
- const actionButtons = row.locator(".actions button");
+ const actionButtons = row.locator(".actions .button");Based on learnings: In src/angular/src/app/pages/files/file.component.html, action controls intentionally use <div class="button"> elements with click guards.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const actionButtons = row.locator(".actions button"); | |
| const btnCount = await actionButtons.count(); | |
| expect(btnCount).toBeGreaterThanOrEqual(1); | |
| const actionButtons = row.locator(".actions .button"); | |
| const btnCount = await actionButtons.count(); | |
| expect(btnCount).toBeGreaterThanOrEqual(1); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/e2e-playwright/tests/dashboard.spec.ts` around lines 108 - 110, The
test's action-button selector is wrong: update the locator used where
actionButtons is assigned (the row.locator(".actions button") call in
dashboard.spec.ts) to match the actual DOM (use ".actions .button") or a
combined selector like ".actions .button, .actions button" so the actionButtons
count check (btnCount >= 1) correctly finds the div.button action controls.
There was a problem hiding this comment.
@nitrobass24, thanks for the correction! Since PR #246 converted the action controls to native <button> elements, the .actions button selector in the E2E test is correct and my comment was based on stale information. I'll update the long-term memory accordingly.
✏️ Learnings added
Learnt from: nitrobass24
Repo: nitrobass24/seedsync PR: 309
File: src/e2e-playwright/tests/dashboard.spec.ts:108-110
Timestamp: 2026-03-23T21:25:23.863Z
Learning: In `src/angular/src/app/pages/files/file.component.html`, all action buttons (Queue, Stop, Extract, Validate, Delete Local, Delete Remote) were converted from `<div class="button">` elements to native `<button>` elements in PR `#246`. The correct selector for Playwright E2E tests targeting these action controls is `.actions button`.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
⛔ Learnings removed
Learnt from: nitrobass24
Repo: nitrobass24/seedsync PR: 236
File: src/angular/src/app/pages/files/file.component.html:157-164
Timestamp: 2026-03-11T15:42:20.132Z
Learning: In `src/angular/src/app/pages/files/file.component.html`, all action buttons (Queue, Stop, Extract, Validate, Delete Local, Delete Remote) intentionally use `<div class="button">` elements with `[attr.disabled]="condition ? null : true"` and click-guard pattern `(click)="!condition || onAction(file())"`. This is a codebase-wide pattern; partial migration to native `<button>` elements should not be done per-button — any accessibility fix must convert all action buttons uniformly in a dedicated PR.
🧠 Learnings used
Learnt from: CR
Repo: nitrobass24/seedsync PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T22:24:33.531Z
Learning: Applies to src/angular/src/**/*.spec.ts : Angular unit tests MUST use Vitest with describe/it syntax, mock services with vi.fn(), use TestBed for component tests, and query DOM by text content or stable attributes rather than positional selectors
Learnt from: nitrobass24
Repo: nitrobass24/seedsync PR: 236
File: src/angular/src/app/pages/files/file.component.html:157-164
Timestamp: 2026-03-11T15:42:20.132Z
Learning: In `src/angular/src/app/pages/files/file.component.html`, all action buttons (Queue, Stop, Extract, Validate, Delete Local, Delete Remote) intentionally use `<div class="button">` elements with `[attr.disabled]="condition ? null : true"` and click-guard pattern `(click)="!condition || onAction(file())"`. This is a codebase-wide pattern; partial migration to native `<button>` elements should not be done per-button — any accessibility fix must convert all action buttons uniformly in a dedicated PR.
| remote_path=self.__context.config.lftp.remote_path, # type: ignore[arg-type] | ||
| local_path=self.__context.config.lftp.local_path, # type: ignore[arg-type] |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider centralizing config validation instead of scattering type ignores.
The # type: ignore[arg-type] comments throughout the file suppress type errors where config values (like remote_address, local_path, etc.) may be typed as Optional[str] but are assumed non-None at runtime.
While this is pragmatic for the Pyright migration, consider adding a config validation method at controller startup that asserts required fields are present. This would:
- Fail fast with a clear error message
- Allow removing many of these ignores
- Make the runtime contract explicit
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/python/controller/controller.py` around lines 296 - 297, Add a startup
validation step that asserts required LFTP config fields are non-None instead of
using scattered "# type: ignore[arg-type]" comments: implement a method like
Controller._validate_lftp_config (or a general Controller.validate_config)
called early in Controller.__init__ or Controller.start that checks
self.__context.config.lftp.remote_address, remote_path, local_path (and any
other Optional[str] fields used later) and raises a clear ValueError (or logs
and exits) if any are missing; once this validation exists, remove the
corresponding type ignores (e.g., on uses of
self.__context.config.lftp.remote_path and local_path) so the runtime contract
is explicit and static typing is satisfied.
CI fixes: - Revert npm ci back to npm install — e2e-playwright has no lockfile - Apply ruff format to controller.py (try/finally re-indentation) Review findings: - Makefile: add test-e2e-docker to help output - path-pairs.page.ts: use waitForURL for SPA navigation in goto() - path-pairs.spec.ts: use label-based selectors in edit test assertions - settings.spec.ts: restore Server Address after mutation, use unique pair name with timestamp suffix - controller.py: require pc parameter in __spawn_move_process (remove unreachable fallback), ruff format fix Skipped (verified against current code): - dashboard.spec.ts .actions button selector is correct — buttons are native <button> elements since PR #246 - controller.py validate_lftp_config — larger refactor, not for this PR - test_path_traversal_rejected assert_not_called — already fixed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/python/controller/controller.py (1)
296-438: 🧹 Nitpick | 🔵 TrivialType ignores are pragmatic but could be consolidated.
The numerous
# type: ignore[arg-type]comments throughout the file suppress errors where config values typed asOptional[str]are used wherestris expected. While pragmatic for the Pyright migration, consider adding a startup validation method that asserts required config fields are present, which would allow removing many of these ignores and provide clearer error messages on misconfiguration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/python/controller/controller.py` around lines 296 - 438, Many of the "# type: ignore[arg-type]" usages come from Optional config fields being used as required in methods like _create_pair_context (accessing __context.config.lftp.remote_path/remote_port/remote_username, __context.config.controller.staging_path, etc.) and _configure_lftp (reading many __context.config.lftp properties); add a startup validation method (e.g., validate_config or _assert_required_config) that checks and raises a clear exception for required lftp/controller/general/validate fields (remote_address/remote_port/remote_username/remote_path, controller.use_staging when staging_path is missing, net_* and pget_* constraints if required, etc.), call it once during Controller initialization before any pair contexts are created, and then remove the corresponding "# type: ignore[arg-type]" comments so those accesses are statically valid and fail fast with clear error messages when misconfigured.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/e2e-playwright/tests/pages/path-pairs.page.ts`:
- Around line 73-75: The selector in getEnabledToggle currently picks the first
checkbox in a row which is fragile; update getEnabledToggle(pairRow: Locator) to
target the checkbox scoped to the "Enabled" label instead of using .first() —
e.g., locate the label text "Enabled" within the given pairRow and select the
associated input[type='checkbox'] so the toggle remains correct even if the
template reorders; adjust the implementation of getEnabledToggle to remove
reliance on .first() and use a label-scoped locator on the provided pairRow
Locator.
In `@src/e2e-playwright/tests/settings.spec.ts`:
- Around line 100-132: The test "select dropdown changes value and saves to
backend" mutates server state but doesn't guarantee restoration if assertions
fail; wrap the interaction and first poll assertion in a try/finally so the
original value is always restored. Specifically, after reading originalFormat
from apiGet("/server/config/get"), perform select.selectOption("json") and the
expect.poll check inside a try block, and move the restore logic
(select.selectOption(String(originalFormat)) and the subsequent expect.poll)
into the finally block to ensure cleanup even on failure; reference the test,
apiGet, settings.getSelect, select.selectOption, and expect.poll when locating
the code to change.
- Around line 63-93: The test "checkbox toggle saves to backend" toggles the
"Enable Debug" checkbox and asserts via apiGet and expect.poll but doesn't
guarantee restoration if the first poll fails; wrap the mutation and its
assertion in a try/finally inside the test so the finally always restores state:
perform the initial await checkbox.click() and the first expect.poll(...) in the
try block, and in the finally click the checkbox back and re-poll via
apiGet/expect.poll to confirm restoration; reference the test function, checkbox
(settings.getCheckbox("Enable Debug")), apiGet("/server/config/get") and
expect.poll calls when making the change.
---
Outside diff comments:
In `@src/python/controller/controller.py`:
- Around line 296-438: Many of the "# type: ignore[arg-type]" usages come from
Optional config fields being used as required in methods like
_create_pair_context (accessing
__context.config.lftp.remote_path/remote_port/remote_username,
__context.config.controller.staging_path, etc.) and _configure_lftp (reading
many __context.config.lftp properties); add a startup validation method (e.g.,
validate_config or _assert_required_config) that checks and raises a clear
exception for required lftp/controller/general/validate fields
(remote_address/remote_port/remote_username/remote_path, controller.use_staging
when staging_path is missing, net_* and pget_* constraints if required, etc.),
call it once during Controller initialization before any pair contexts are
created, and then remove the corresponding "# type: ignore[arg-type]" comments
so those accesses are statically valid and fail fast with clear error messages
when misconfigured.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bbbc2a19-5d3f-41eb-b081-b4f8f1ef95b0
📒 Files selected for processing (6)
.github/workflows/ci.ymlMakefilesrc/e2e-playwright/tests/pages/path-pairs.page.tssrc/e2e-playwright/tests/path-pairs.spec.tssrc/e2e-playwright/tests/settings.spec.tssrc/python/controller/controller.py
- path-pairs.page.ts: scope getEnabledToggle to "Enabled" label instead of fragile .first() positional selector - settings.spec.ts: wrap checkbox and select dropdown tests in try/finally to guarantee state restoration on assertion failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
src/e2e-playwright/tests/settings.spec.ts (2)
40-44:⚠️ Potential issue | 🟡 MinorMove state mutations inside
tryblocksAt Line 40/42, Line 110, and Line 206/207, mutation happens before
try. If one of these actions throws,finallywon’t run and test state may remain dirty.♻️ Suggested fix pattern
- await field.clear(); - const testValue = "e2e-test-server"; - await field.fill(testValue); - - try { + const testValue = "e2e-test-server"; + try { + await field.clear(); + await field.fill(testValue); await expect .poll(Also applies to: 110-113, 206-210
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/e2e-playwright/tests/settings.spec.ts` around lines 40 - 44, The mutations to test state (e.g., calling await field.clear() and await field.fill(testValue) using the local variable testValue) are executed before the surrounding try blocks so if they throw the finally cleanup won’t run; move those state-mutating calls (await field.clear(), await field.fill(testValue), and the analogous mutations at the other occurrences around lines referencing field/testValue) inside their respective try blocks (so the try begins before any await that changes page state) and leave only cleanup in finally to guarantee cleanup runs even on failure.
175-175:⚠️ Potential issue | 🟡 MinorUse a stronger unique suffix for path-pair names
At Line 175,
Date.now()alone can still collide under parallel workers/retries. Add a random suffix as well.♻️ Suggested fix
- const pairName = `temp-pair-${Date.now()}`; + const pairName = `temp-pair-${Date.now()}-${Math.random() + .toString(36) + .slice(2, 8)}`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/e2e-playwright/tests/settings.spec.ts` at line 175, The generated pair name using only Date.now() in the const pairName declaration is prone to collisions in parallel tests; update the pairName construction (variable name: pairName) to append a compact cryptographically-safe random suffix (e.g., use crypto.randomBytes or a short base36 Math.random-based token) so the final string is like `temp-pair-${Date.now()}-${randomSuffix}` and ensure the suffix uses URL-safe characters to avoid path issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/e2e-playwright/tests/pages/path-pairs.page.ts`:
- Around line 26-30: getPairByName currently uses substring matching via hasText
and can return wrong rows (e.g., "Prod" matching "Prod-Backup"); update
getPairByName to locate ".pair-row" but require its ".pair-name" child to match
the provided name exactly using a regex anchored with ^ and $ (use a RegExp
built from the input). Also add/use an escapeRegExp helper to escape
user-supplied name before creating the RegExp so names with regex metacharacters
don't break matching.
In `@src/e2e-playwright/tests/settings.spec.ts`:
- Around line 57-59: The cleanup currently only calls apiSetConfig("lftp",
"remote_address", originalAddress) when originalAddress is truthy, which skips
restoring empty-string values and can leak config between tests; change the
teardown to always call apiSetConfig with originalAddress (remove the
surrounding if check) so apiSetConfig("lftp", "remote_address", originalAddress)
runs unconditionally; do the same for the other restoration block referenced
(the variables used there and the apiSetConfig call around lines 213-215) so
empty-string or falsy original values are correctly restored.
---
Duplicate comments:
In `@src/e2e-playwright/tests/settings.spec.ts`:
- Around line 40-44: The mutations to test state (e.g., calling await
field.clear() and await field.fill(testValue) using the local variable
testValue) are executed before the surrounding try blocks so if they throw the
finally cleanup won’t run; move those state-mutating calls (await field.clear(),
await field.fill(testValue), and the analogous mutations at the other
occurrences around lines referencing field/testValue) inside their respective
try blocks (so the try begins before any await that changes page state) and
leave only cleanup in finally to guarantee cleanup runs even on failure.
- Line 175: The generated pair name using only Date.now() in the const pairName
declaration is prone to collisions in parallel tests; update the pairName
construction (variable name: pairName) to append a compact
cryptographically-safe random suffix (e.g., use crypto.randomBytes or a short
base36 Math.random-based token) so the final string is like
`temp-pair-${Date.now()}-${randomSuffix}` and ensure the suffix uses URL-safe
characters to avoid path issues.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: dca3812b-f30b-4044-9fec-f1816d64f44d
📒 Files selected for processing (2)
src/e2e-playwright/tests/pages/path-pairs.page.tssrc/e2e-playwright/tests/settings.spec.ts
…eanup - getPairByName uses exact regex match on .pair-name to prevent substring collisions - Move mutations inside try blocks so finally always restores original state - Use unconditional restore with ?? "" fallback for empty/null original values Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/e2e-playwright/tests/settings.spec.ts (3)
106-123:⚠️ Potential issue | 🟡 MinorInclude
selectOption("json")insidetryfor robust state restoration.At Line 107, the config mutation happens before
try/finally. Move it insidetryso cleanup is guaranteed even if interaction fails.♻️ Suggested fix
const select = settings.getSelect("Log Format"); - await select.selectOption("json"); - - try { + try { + await select.selectOption("json"); // Poll the API until the value is saved await expect .poll(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/e2e-playwright/tests/settings.spec.ts` around lines 106 - 123, The selection of the "json" log format happens before the try/finally, risking not restoring state on failures; move the call to select.selectOption("json") into the try block (immediately before the expect.poll call) so that the finally block that restores using select.selectOption(String(originalFormat)) always runs, keeping the rest of the polling logic (apiGet("/server/config/get") and expect.poll(...).toBe("json")) unchanged.
172-172:⚠️ Potential issue | 🟠 MajorStrengthen temporary pair name uniqueness to avoid cross-worker collisions.
Line 172 uses only
Date.now(). In parallel runs, same-millisecond collisions can still happen and cause nondeterministic failures.♻️ Suggested fix
- const pairName = `temp-pair-${Date.now()}`; + const pairName = `temp-pair-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/e2e-playwright/tests/settings.spec.ts` at line 172, The temporary pairName currently uses only Date.now() which can collide across parallel workers; update the generation of pairName (where const pairName = `temp-pair-${Date.now()}`) to include additional entropy such as process.pid and a short random/UUID segment (for example `temp-pair-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2,8)}` or a UUID) so names are unique across workers and runs; modify the assignment in settings.spec.ts where pairName is defined to use this stronger unique pattern.
65-81:⚠️ Potential issue | 🟡 MinorMove the checkbox mutation into the
tryblock to guarantee cleanup execution.At Line 65,
checkbox.click()runs beforetry/finallystarts (Line 67). If that interaction throws after changing state, restoration may be skipped.♻️ Suggested fix
- await checkbox.click(); - - try { + try { + await checkbox.click(); // Poll the API until the value is saved await expect .poll(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/e2e-playwright/tests/settings.spec.ts` around lines 65 - 81, Move the initial mutation (await checkbox.click()) inside the try block so the finally cleanup always runs even if the click or subsequent polling throws; specifically, in the test around the try/finally that uses expect.poll and apiGet (the block referencing checkbox.click(), expect.poll(... apiGet("/server/config/get") ...).toBe(expected)), call await checkbox.click() at the start of the try block (before the expect.poll) and keep await checkbox.click() in the finally to toggle back.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/e2e-playwright/tests/settings.spec.ts`:
- Around line 192-194: The cleanup DELETE currently calls
apiFetch(`/server/pathpairs/${pair.id}`, { method: "DELETE" }) and ignores the
result; update the cleanup to await the apiFetch response, check the HTTP status
or returned success flag from the response (from the apiFetch call) and
throw/assert if deletion failed so test data doesn't leak; locate the block
using pair?.id and apiFetch in settings.spec.ts and add a failure check (e.g.,
assert status is 200/204 or response.ok) and log/throw a descriptive error if
the delete did not succeed.
---
Duplicate comments:
In `@src/e2e-playwright/tests/settings.spec.ts`:
- Around line 106-123: The selection of the "json" log format happens before the
try/finally, risking not restoring state on failures; move the call to
select.selectOption("json") into the try block (immediately before the
expect.poll call) so that the finally block that restores using
select.selectOption(String(originalFormat)) always runs, keeping the rest of the
polling logic (apiGet("/server/config/get") and expect.poll(...).toBe("json"))
unchanged.
- Line 172: The temporary pairName currently uses only Date.now() which can
collide across parallel workers; update the generation of pairName (where const
pairName = `temp-pair-${Date.now()}`) to include additional entropy such as
process.pid and a short random/UUID segment (for example
`temp-pair-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2,8)}`
or a UUID) so names are unique across workers and runs; modify the assignment in
settings.spec.ts where pairName is defined to use this stronger unique pattern.
- Around line 65-81: Move the initial mutation (await checkbox.click()) inside
the try block so the finally cleanup always runs even if the click or subsequent
polling throws; specifically, in the test around the try/finally that uses
expect.poll and apiGet (the block referencing checkbox.click(), expect.poll(...
apiGet("/server/config/get") ...).toBe(expected)), call await checkbox.click()
at the start of the try block (before the expect.poll) and keep await
checkbox.click() in the finally to toggle back.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c8500999-d696-4711-af48-542625c7439f
📒 Files selected for processing (2)
src/e2e-playwright/tests/pages/path-pairs.page.tssrc/e2e-playwright/tests/settings.spec.ts
…e names
- Move select.selectOption("json") inside try block for safe finally restore
- Assert DELETE response in path pair cleanup to catch leaked test data
- Add random suffix to temp pair name for cross-worker uniqueness
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
local_size/remote_sizecorrectly typed asnumber | nullto match backend contractLinked Issues
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Security
Tests
Chores